/*
 * Copyright (C) 2010-2011 Comcast Cable.  All Rights Reserved.
 */
package com.hercules.android.receiver.handlers.commands;

import com.hercules.android.receiver.AndroidReceiver;
import com.hercules.android.receiver.AndroidReceiverException;
import com.hercules.android.receiver.ApplicationData;
import com.hercules.android.receiver.LogUtil;
import com.hercules.android.receiver.model.commands.XRECallCommand;
import com.hercules.android.receiver.model.commands.XRECommand;
import com.hercules.android.receiver.model.commands.XREConnectCommand;
import com.hercules.android.receiver.model.commands.XREDeleteCommand;
import com.hercules.android.receiver.model.commands.XRESetCommand;
import com.hercules.android.receiver.model.commands.XRETargetCommand;
import com.hercules.android.receiver.model.objects.XREObject;
import com.hercules.android.receiver.util.evaluators.InvalidAndroidReceiverStateException;
import com.hercules.android.receiver.util.evaluators.InvalidPathException;
import com.hercules.android.receiver.util.evaluators.PathEvaluator;

import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;

/**
 * This handler has a map of command handlers, to which it delegated the handling of the message. The command handlers
 * are mapped according to the command type.
 * <p>
 * This handler will check the command index to see if it is proper (if not it will increment the number of bad
 * commands). If maxBadCommandAcceptCount value from the receiver is reached, it will call shutdown() method from the
 * receiver. Then, it will delegate to the proper command handler, according to the command type found in argument's
 * "command" attribute.
 * <p>
 * If the command has paths instead of ids for some of the direct properties, the handler will use the XREPathEvaluator
 * to get the id and will set it in the corresponding id property of the command.
 * <p>
 * If any error occurs while handling the command it will use XRECommandErrorHandler to process the error.
 * <p>
 * <p>
 * <b>Sample Setup:</b>
 *
 * <pre>
 * // prepare auxiliary handlers
 * AuxiliaryXRECommandHandler preCommandMessageHandler = new MockAuxiliaryHandler();
 * AuxiliaryXRECommandHandler postCommandMessageHandler = new MockAuxiliaryHandler();
 * AuxiliaryXRECommandHandler connectRejectedCommandAuxiliaryHandler = new MockAuxiliaryHandler();
 * AuxiliaryXRECommandHandler redirectCommandAuxiliaryHandler = new MockAuxiliaryHandler();
 * AuxiliaryXRECommandHandler restartCommandAuxiliaryHandler = new MockAuxiliaryHandler();
 * AuxiliaryXRECommandHandler prePaintingHandler = new MockAuxiliaryHandler();
 *
 * // prepare object painters
 * XREObjectPainter applicationPainter = new MockPainter();
 * Map&lt;String, XREObjectPainter&gt; objectPainters = new HashMap&lt;String, XREObjectPainter&gt;();
 * objectPainters.put(&quot;XREView&quot;, new MockPainter());
 * objectPainters.put(&quot;XREImage&quot;, new MockPainter());
 *
 * // prepare path and expressions evaluators
 * PathEvaluator pathEvaluator = new XREPathEvaluator(null);
 * ExpressionEvaluator expressionEvaluator = new XREExpressionEvaluator(null, pathEvaluator);
 *
 * // prepare error handler
 * XRECommandErrorHandler errorHandler = new MockErrorHandler();
 *
 * // create XREConnectCommandHandler
 * XREConnectCommandHandler connectCommandHandler = new XREConnectCommandHandler();
 * // configure XREConnectCommandHandler
 * connectCommandHandler.setApplicationPainter(applicationPainter);
 * connectCommandHandler.setPreCommandMessageHandler(preCommandMessageHandler);
 * connectCommandHandler.setPostCommandMessageHandler(postCommandMessageHandler);
 * connectCommandHandler.setLogger(Logger.getLogger(&quot;XREConnectCommandHandler&quot;));
 *
 * // create XREShutdownCommandHandler
 * XREShutdownCommandHandler shutdownCommandHandler = new XREShutdownCommandHandler();
 * // configure XREShutdownCommandHandler
 * shutdownCommandHandler.setApplicationPainter(applicationPainter);
 * shutdownCommandHandler.setPreCommandMessageHandler(preCommandMessageHandler);
 * shutdownCommandHandler.setPostCommandMessageHandler(postCommandMessageHandler);
 * shutdownCommandHandler.setLogger(Logger.getLogger(&quot;XREShutdownCommandHandler&quot;));
 *
 * // create XREConnectRejectedCommandHandler
 * XREConnectRejectedCommandHandler connectRejectedCommandHandler = new XREConnectRejectedCommandHandler();
 * // configure XREConnectRejectedCommandHandler
 * connectRejectedCommandHandler.setConnectRejectedCommandAuxiliaryHandler(connectRejectedCommandAuxiliaryHandler);
 * connectRejectedCommandHandler.setPreCommandMessageHandler(preCommandMessageHandler);
 * connectRejectedCommandHandler.setPostCommandMessageHandler(postCommandMessageHandler);
 * connectRejectedCommandHandler.setLogger(Logger.getLogger(&quot;XREConnectRejectedCommandHandler&quot;));
 *
 * // create XRERedirectCommandHandler
 * XRERedirectCommandHandler redirectCommandHandler = new XRERedirectCommandHandler();
 * // configure XRERedirectCommandHandler
 * redirectCommandHandler.setRedirectCommandAuxiliaryHandler(redirectCommandAuxiliaryHandler);
 * redirectCommandHandler.setPreCommandMessageHandler(preCommandMessageHandler);
 * redirectCommandHandler.setPostCommandMessageHandler(postCommandMessageHandler);
 * redirectCommandHandler.setLogger(Logger.getLogger(&quot;XRERedirectCommandHandler&quot;));
 *
 * // create XRERestartCommandHandler
 * XRERestartCommandHandler restartCommandHandler = new XRERestartCommandHandler();
 * // configure XRERestartCommandHandler
 * restartCommandHandler.setRestartCommandAuxiliaryHandler(restartCommandAuxiliaryHandler);
 * restartCommandHandler.setPreCommandMessageHandler(preCommandMessageHandler);
 * restartCommandHandler.setPostCommandMessageHandler(postCommandMessageHandler);
 * restartCommandHandler.setLogger(Logger.getLogger(&quot;XRERestartCommandHandler&quot;));
 *
 * // create XREViewFinderUtility
 * XREViewFinderUtility affectedViewFinder = new DefaultXREViewFinderUtility();
 *
 * // create XRENewCommandHandler
 * XRENewCommandHandler newCommandHandler = new XRENewCommandHandler();
 * // configure XRENewCommandHandler
 * newCommandHandler.setPrePaintingHandler(prePaintingHandler);
 * newCommandHandler.setObjectPainters(objectPainters);
 * newCommandHandler.setAffectedViewFinder(affectedViewFinder);
 * newCommandHandler.setPreCommandMessageHandler(preCommandMessageHandler);
 * newCommandHandler.setPostCommandMessageHandler(postCommandMessageHandler);
 * newCommandHandler.setLogger(Logger.getLogger(&quot;XRENewCommandHandler&quot;));
 *
 * // create XRESetCommandHandler
 * XRESetCommandHandler setCommandHandler = new XRESetCommandHandler();
 * // configure XRESetCommandHandler
 * setCommandHandler.setExpressionEvaluator(expressionEvaluator);
 * setCommandHandler.setPrePaintingHandler(prePaintingHandler);
 * setCommandHandler.setObjectPainters(objectPainters);
 * setCommandHandler.setAffectedViewFinder(affectedViewFinder);
 * setCommandHandler.setPreCommandMessageHandler(preCommandMessageHandler);
 * setCommandHandler.setPostCommandMessageHandler(postCommandMessageHandler);
 * setCommandHandler.setLogger(Logger.getLogger(&quot;XRESetCommandHandler&quot;));
 *
 * // create XREDeleteCommandHandler
 * XREDeleteCommandHandler deleteCommandHandler = new XREDeleteCommandHandler();
 * // configure XREDeleteCommandHandler
 * deleteCommandHandler.setPrePaintingHandler(prePaintingHandler);
 * deleteCommandHandler.setObjectPainters(objectPainters);
 * deleteCommandHandler.setAffectedViewFinder(affectedViewFinder);
 * deleteCommandHandler.setPreCommandMessageHandler(preCommandMessageHandler);
 * deleteCommandHandler.setPostCommandMessageHandler(postCommandMessageHandler);
 * deleteCommandHandler.setLogger(Logger.getLogger(&quot;XREDeleteCommandHandler&quot;));
 *
 * // create XREAggregateCommandHandler
 * aggregateCommandHandler = new XREAggregateCommandHandler();
 *
 * // configure XREAggregateCommandHandler
 * Map&lt;String, XRECommandHandler&gt; commandHandlers = new HashMap&lt;String, XRECommandHandler&gt;();
 * commandHandlers.put(&quot;CONNECT&quot;, connectCommandHandler);
 * commandHandlers.put(&quot;CONNECT_REJECTED&quot;, connectRejectedCommandHandler);
 * commandHandlers.put(&quot;SHUTDOWN&quot;, shutdownCommandHandler);
 * commandHandlers.put(&quot;REDIRECT&quot;, redirectCommandHandler);
 * commandHandlers.put(&quot;RESTART&quot;, restartCommandHandler);
 * commandHandlers.put(&quot;NEW&quot;, newCommandHandler);
 * commandHandlers.put(&quot;SET&quot;, setCommandHandler);
 * commandHandlers.put(&quot;DELETE&quot;, deleteCommandHandler);
 * aggregateCommandHandler.setCommandHandlers(commandHandlers);
 * aggregateCommandHandler.setPathEvaluator(pathEvaluator);
 * aggregateCommandHandler.setErrorHandler(errorHandler);
 * aggregateCommandHandler.setLogger(Logger.getLogger(&quot;XREAggregateCommandHandler&quot;));
 * </pre>
 * <p>
 * <b>Sample Usage:</b>
 *
 * <pre>
 * int cmdIndex = 1;
 * // prepare AndroidReceiver
 * AndroidReceiver receiver = new MockReceiver();
 *
 * // setup aggregate command handler
 * testSetupHandlers();
 *
 * // set into AndroidReceiver:
 * receiver.setCommandHandler(aggregateCommandHandler);
 *
 * // connect the receiver to host:
 * receiver.connect();
 *
 * // on successful connection the host should send CONNECT command:
 * // {
 * // command: &quot;CONNECT&quot;,
 * // version: &quot;1.2.3&quot;,
 * // sessionGUID: &quot;1111-1111-1111&quot;,
 * // commandIndex: 0,
 * // timestamp: 12393187203230
 * // }
 * // the aggregateCommandHandler will invoke XREConnectCommandHandler to handle CONNECT command
 * XREConnectCommand connectCommand = new XREConnectCommand();
 * connectCommand.setCommandIndex(cmdIndex++);
 * connectCommand.setSessionGUID(&quot;1111-1111-1111&quot;);
 * connectCommand.setVersion(&quot;1.2.3&quot;);
 * aggregateCommandHandler.onCommandMessage(connectCommand, receiver);
 * Map&lt;Integer, XREObject&gt; elementsMap = receiver.getApplicationData().getElementsXREIdToObject();
 * receiver.getApplicationData().setParentReceiver(new MockReceiver());
 *
 * // after handling, the version should be set to &quot;1.2.3&quot;:
 * System.out.println(&quot;Version: &quot; + receiver.getApplicationData().getVersion());
 * // the sessionGUID should be set to &quot;1111-1111-1111&quot;:
 * System.out.println(&quot;Session GUID: &quot; + receiver.getApplicationData().getSessionGUID());
 * // the currentCommandIndex should be set to 1:
 * System.out.println(&quot;Command index: &quot; + receiver.getApplicationData().getCurrentCommandIndex());
 * // the root view should be created with ID as XREObject.ID_ROOT_VIEW:
 * System.out.println(&quot;Root view: &quot; + elementsMap.get(XREObject.ID_ROOT_VIEW));
 * // the root application should be created with ID as XREObject.ID_ROOT_APPLICATION:
 * System.out.println(&quot;Root application: &quot; + elementsMap.get(XREObject.ID_ROOT_APPLICATION));
 *
 * // assuming the host send NEW command to create an XREView:
 * // {
 * // command: &quot;NEW&quot;,
 * // id: 2050,
 * // klass: &quot;XREView&quot;,
 * // params: { dimensions: [600, 600], parent: 2 },
 * // commandIndex: 1,
 * // timestamp: 12393187203231
 * // }
 * // then the aggregateCommandHandler will invoke XRENewCommandHandler to handle NEW command:
 * XRENewCommand newCommand = new XRENewCommand();
 * newCommand.setId(2050);
 * newCommand.setKlass(XREView.class.getName());
 * newCommand.setParams(params(&quot;dimensions&quot;, new int[]{600, 600}, &quot;parent&quot;, 2));
 * newCommand.setCommandIndex(cmdIndex++);
 * aggregateCommandHandler.onCommandMessage(newCommand, receiver);
 * // after handling, the XREView should be created with ID 2050:
 * XREView view = (XREView) elementsMap.get(2050);
 * // the parent should be set to XREView[2]
 * System.out.println(&quot;XREView id: &quot; + view.getParent().getId());
 * // the dimensions should be set to [600, 600]
 * System.out.println(&quot;XREView dimensions: &quot; + view.getDimensions()[0] + &quot;, &quot; + view.getDimensions()[1]);
 *
 * // assuming the host send NEW command to create an XRECommandSequence for client execution:
 * // {
 * // command: &quot;NEW&quot;,
 * // id: 2051,
 * // klass: &quot;XRECommandSequence&quot;,
 * // params: { commands: [
 * // {
 * // command: &quot;SET&quot;,
 * // target: &quot;this&quot;
 * // props: { width: 500 }
 * // },
 * // {
 * // command: &quot;SET&quot;,
 * // target: &quot;this&quot;
 * // props: { alpha: &quot;{this.alpha 2 /}&quot; }
 * // }
 * // ] },
 * // commandIndex: 2,
 * // timestamp: 12393187203232
 * // }
 * // then the aggregateCommandHandler will invoke XRENewCommandHandler to handle NEW command:
 * XRESetCommand command0 = new XRESetCommand();
 * command0.setTargetPath(&quot;this.children[0]&quot;);
 * command0.setProps(params(&quot;width&quot;, 500));
 * XRESetCommand command1 = new XRESetCommand();
 * command1.setTargetPath(&quot;this.children[0]&quot;);
 * command1.setRuntimeExpressionProperties(runtime(&quot;alpha&quot;, &quot;{this.alpha 2 /}&quot;));
 * newCommand = new XRENewCommand();
 * newCommand.setCommandIndex(cmdIndex++);
 * newCommand.setId(2051);
 * newCommand.setKlass(XRECommandSequence.class.getName());
 * newCommand.setParams(params(&quot;commands&quot;, Arrays.asList(command0, command1)));
 * aggregateCommandHandler.onCommandMessage(newCommand, receiver);
 * // after handling, the XRECommandSequence should be created with ID 2051:
 * XRECommandSequence commandSequence = (XRECommandSequence) elementsMap.get(2051);
 * // the commandSequence should have 2 commands
 * System.out.println(&quot;Command sequence size: &quot; + commandSequence.getCommands().size());
 *
 * // assuming the host send SET command to install a client event handler for XREView[2050]:
 * // {
 * // command: &quot;SET&quot;,
 * // targetId: 2050,
 * // props: { &quot;onMouseIn&quot;: 2051 },
 * // commandIndex: 3,
 * // timestamp: 12393187203233
 * // }
 * // then the aggregateCommandHandler will invoke XRESetCommandHandler to handle SET command:
 * XRESetCommand setCommand = new XRESetCommand();
 * setCommand.setTargetId(2050);
 * setCommand.setCommandIndex(cmdIndex++);
 * setCommand.setProps(params(&quot;onMouseIn&quot;, 2051));
 * aggregateCommandHandler.onCommandMessage(setCommand, receiver);
 * // after handling, the XREView[2050] should have an event handler setup for &quot;onMouseIn&quot; event:
 * XREEventHandler eventHandler = view.getEventHandlers().get(&quot;onMouseIn&quot;);
 * // the event handler should have XRECommandSequence[2051]:
 * System.out.println(&quot;Command sequence ID: &quot; + eventHandler.getCommandSequence().getId());
 *
 * // assuming user moves the mouse into XREView[2050],
 * // the XRECommandSequence[2051] of eventHandler will be executed:
 * for (XRECommand clientCommand : eventHandler.getCommandSequence().getCommands()) {
 *     aggregateCommandHandler.onCommandMessage(clientCommand, receiver);
 * }
 * // after that the view's width is set to 500 and alpha is set to 0.5
 * System.out.println(&quot;New width: &quot; + view.getDimensions()[1]);
 * System.out.println(&quot;New alpha: &quot; + view.getAlpha());
 *
 * // assuming the host send DELETE command to delete XRECommandSequence[2051]:
 * // {
 * // command: &quot;DELETE&quot;,
 * // targetId: 2051,
 * // commandIndex: 4,
 * // timestamp: 12393187203234
 * // }
 * // then the aggregateCommandHandler will invoke XREDeleteCommandHandler to handle DELETE command:
 * XREDeleteCommand deleteCommand = new XREDeleteCommand();
 * deleteCommand.setTargetId(2051);
 * deleteCommand.setCommandIndex(cmdIndex++);
 * aggregateCommandHandler.onCommandMessage(deleteCommand, receiver);
 * // after handling, the XRECommandSequence[2051] should be deleted:
 * System.out.println(&quot;Command sequence deleted: &quot; + elementsMap.get(2051) == null);
 * // the XREView[2050] should have the event handler uninstalled for &quot;onMouseIn&quot; event:
 * System.out.println(&quot;Command sequence uninstalled: &quot; + view.getEventHandlers().get(&quot;onMouseIn&quot;) == null);
 *
 * // assuming the host send NEW command to create an XREFont:
 * // {
 * // command: &quot;NEW&quot;,
 * // id: 2052,
 * // klass: &quot;XREFont&quot;,
 * // params: { style: &quot;ITALIC&quot; },
 * // commandIndex: 5,
 * // timestamp: 12393187203235
 * // }
 * // then the aggregateCommandHandler will invoke XRENewCommandHandler to handle NEW command:
 * newCommand = new XRENewCommand();
 * newCommand.setId(2052);
 * newCommand.setKlass(XREFont.class.getName());
 * newCommand.setParams(params(&quot;style&quot;, &quot;ITALIC&quot;));
 * newCommand.setCommandIndex(cmdIndex++);
 * aggregateCommandHandler.onCommandMessage(newCommand, receiver);
 * // after handling, the XREFont should be created with ID 2052:
 * XREFont font = (XREFont) elementsMap.get(2052);
 * // the font style should be set to &quot;ITALIC&quot;
 * System.out.println(&quot;Style of created font: &quot; + font.getStyle());
 *
 * // assuming the host send NEW command to create an XREText:
 * // {
 * // command: &quot;NEW&quot;,
 * // id: 2053,
 * // klass: &quot;XREText&quot;,
 * // params: { text: &quot;Hello world&quot;, font: 2052 },
 * // commandIndex: 6,
 * // timestamp: 12393187203236
 * // }
 * // then the aggregateCommandHandler will invoke XRENewCommandHandler to handle NEW command:
 * newCommand = new XRENewCommand();
 * newCommand.setId(2053);
 * newCommand.setKlass(XREText.class.getName());
 * newCommand.setParams(params(&quot;text&quot;, &quot;Hello world&quot;, &quot;font&quot;, 2052));
 * newCommand.setCommandIndex(cmdIndex++);
 * aggregateCommandHandler.onCommandMessage(newCommand, receiver);
 * // after handling, the XREText should be created with ID 2053:
 * XREText text = (XREText) elementsMap.get(2053);
 * // the text string should be set to &quot;Hello world&quot;
 * System.out.println(&quot;Text: &quot; + text.getText());
 * // the text's font should be set to XREFont[2052]
 * System.out.println(&quot;Font ID: &quot; + text.getFont().getId());
 *
 * // assuming the host send SET command to associate the XREText[2053] to XREView[2050]:
 * // {
 * // command: &quot;SET&quot;,
 * // targetId: 2050,
 * // props: { resource: 2053 },
 * // commandIndex: 7,
 * // timestamp: 12393187203237
 * // }
 * // then the aggregateCommandHandler will invoke XRESetCommandHandler to handle SET command:
 * setCommand = new XRESetCommand();
 * setCommand.setTargetId(2050);
 * setCommand.setProps(params(&quot;resource&quot;, 2053));
 * setCommand.setCommandIndex(cmdIndex++);
 * aggregateCommandHandler.onCommandMessage(setCommand, receiver);
 * // after handling, the XREView[2050] should be associated with XREText[2053]:
 * System.out.println(&quot;View's resource ID: &quot; + view.getResource().getId());
 * // also the XREView[2050] with its associated resource XREText[2053] will be painted on screen
 *
 * // assuming the host send SET command to update the XREFont[2052]:
 * // {
 * // command: &quot;SET&quot;,
 * // targetId: 2052,
 * // props: { style: &quot;BOLD&quot; },
 * // commandIndex: 8,
 * // timestamp: 12393187203238
 * // }
 * // then the aggregateCommandHandler will invoke XRESetCommandHandler to handle SET command:
 * setCommand = new XRESetCommand();
 * setCommand.setTargetId(2052);
 * setCommand.setProps(params(&quot;style&quot;, &quot;BOLD&quot;));
 * setCommand.setCommandIndex(cmdIndex++);
 * aggregateCommandHandler.onCommandMessage(setCommand, receiver);
 * // after handling, the XREFont[2052] should be updated to have style &quot;BOLD&quot;:
 * System.out.println(&quot;Changed style: &quot; + font.getStyle());
 * // also the XREView[2050] with its associated resource XREText[2053] will be repainted on screen
 *
 * // assuming the host send DELETE command to delete XREText[2053]:
 * // {
 * // command: &quot;DELETE&quot;,
 * // targetId: 2053,
 * // commandIndex: 9,
 * // timestamp: 12393187203239
 * // }
 * // then the aggregateCommandHandler will invoke XREDeleteCommandHandler to handle DELETE command:
 * deleteCommand = new XREDeleteCommand();
 * deleteCommand.setTargetId(2053);
 * deleteCommand.setCommandIndex(cmdIndex++);
 * aggregateCommandHandler.onCommandMessage(deleteCommand, receiver);
 * // after handling, the XREText[2053] should be deleted:
 * System.out.println(&quot;Text is deleted: &quot; + elementsMap.get(2053) == null);
 * // the XREView[2050] should have no resource associated:
 * System.out.println(view.getResource() == null);
 * // also the XREView[2050] will be repainted on screen
 *
 * // assuming the host send RESTART command to restart the application:
 * // {
 * // command: &quot;RESTART&quot;,
 * // reason: &quot;Application version updated&quot;,
 * // statusCode: 501,
 * // commandIndex: 10,
 * // timestamp: 12393187203240
 * // }
 * // then the aggregateCommandHandler will invoke XRERestartCommandHandler to handle RESTART command:
 * XRERestartCommand restartCommand = new XRERestartCommand();
 * restartCommand.setReason(&quot;Application version updated&quot;);
 * restartCommand.setStatusCode(501);
 * restartCommand.setCommandIndex(cmdIndex++);
 * aggregateCommandHandler.onCommandMessage(restartCommand, receiver);
 * // the receiver will shutdown with its state cleared and reset, and then reconnected.
 * cmdIndex = 1;
 *
 * // then on successful restart the host should send CONNECT command back:
 * // {
 * // command: &quot;CONNECT&quot;,
 * // version: &quot;1.2.4&quot;,
 * // sessionGUID: &quot;2222-2222-2222&quot;,
 * // commandIndex: 0,
 * // timestamp: 12393187203241
 * // }
 * // then the aggregateCommandHandler will invoke XREConnectCommandHandler to handle CONNECT command:
 * connectCommand = new XREConnectCommand();
 * connectCommand.setVersion(&quot;1.2.4&quot;);
 * connectCommand.setSessionGUID(&quot;2222-2222-2222&quot;);
 * connectCommand.setCommandIndex(cmdIndex++);
 * aggregateCommandHandler.onCommandMessage(connectCommand, receiver);
 * // after handling, the version should be set to &quot;1.2.4&quot;:
 * System.out.println(&quot;Version: &quot; + receiver.getApplicationData().getVersion());
 * // the sessionGUID should be set to &quot;2222-2222-2222&quot;:
 * System.out.println(&quot;Session GUID: &quot; + receiver.getApplicationData().getSessionGUID());
 * // the currentCommandIndex should be set to 0:
 * System.out.println(&quot;Command index: &quot; + receiver.getApplicationData().getCurrentCommandIndex());
 *
 * // assuming the host send REDIRECT command to redirect to a new URL:
 * // {
 * // command: &quot;REDIRECT&quot;,
 * // reason: &quot;Application load balance&quot;,
 * // statusCode: 502,
 * // url: &quot;xre://[host]:[port]/[application]&quot;
 * // preserveSession: true,
 * // commandIndex: 1,
 * // timestamp: 12393187203242
 * // }
 * // then the aggregateCommandHandler will invoke XRERedirectCommandHandler to handle REDIRECT command:
 * XRERedirectCommand redirectCommand = new XRERedirectCommand();
 * redirectCommand.setReason(&quot;Application load balance&quot;);
 * redirectCommand.setStatusCode(502);
 * redirectCommand.setUrl(&quot;xre://[host]:[port]/[application]&quot;);
 * redirectCommand.setPreserveSession(true);
 * redirectCommand.setCommandIndex(cmdIndex++);
 * aggregateCommandHandler.onCommandMessage(redirectCommand, receiver);
 * // the receiver will shutdown with its state preserved(e.g. the current command index and
 * // session GUID), and then connected to the new URL.
 *
 * // assuming the host on new URL send CONNECT_REJECTED command to reject the connection:
 * // {
 * // command: &quot;CONNECT_REJECTED&quot;,
 * // reason: &quot;Application server busy&quot;,
 * // statusCode: 503,
 * // commandIndex: 2,
 * // timestamp: 12393187203243
 * // }
 * // then the aggregateCommandHandler will invoke
 * // XREConnectRejectedCommandHandler to handle CONNECT_REJECTED command:
 * XREConnectRejectedCommand rejectedCommand = new XREConnectRejectedCommand();
 * rejectedCommand.setReason(&quot;Application server busy&quot;);
 * rejectedCommand.setStatusCode(503);
 * rejectedCommand.setCommandIndex(cmdIndex++);
 * aggregateCommandHandler.onCommandMessage(rejectedCommand, receiver);
 * // the the configured connectRejectedCommandAuxiliaryHandler will be called.
 * // assuming the connectRejectedCommandAuxiliaryHandler will try to reconnect the receiver
 *
 * // assuming the host send SHUTDOWN command to explicitly shutdown the receiver:
 * // {
 * // command: &quot;SHUTDOWN&quot;,
 * // commandIndex: 3,
 * // timestamp: 12393187203244
 * // }
 * // then the aggregateCommandHandler will invoke XREShutdownCommandHandler to handle SHUTDOWN command:
 * XREShutdownCommand shutdownCommand = new XREShutdownCommand();
 * shutdownCommand.setCommandIndex(cmdIndex++);
 * aggregateCommandHandler.onCommandMessage(shutdownCommand, receiver);
 * // the receiver.onShutdownCommand(false) method will be called to shutdown the receiver
 * </pre>
 *
 * <b>Thread Safety:</b> This class is not thread safe since it has mutable fields. But if they are not changed after
 * first configured, then this class is used in a thread safe manner.
 *
 * @author liuliquan, 7realm
 * @author 7realm
 * @version 1.0
 */
public class XREAggregateCommandHandler {
    /**
     * The map from command type to command handlers.
     * <p>
     * Initial value is null. Not null. Not empty. The key must be not null and not empty. The value must be not null.
     * It has getter and setter.
     */
    private Map<String, XRECommandHandler> commandHandlers;

    /**
     * The handler used to process error.
     * <p>
     * Initial value is null. Not null. It has getter and setter.
     */
    private XRECommandErrorHandler errorHandler;

    /**
     * The evaluator used to evaluate the DOM path for XREObject.
     * <p>
     * Initial value is null. Not null. It has getter and setter.
     */
    private PathEvaluator pathEvaluator;

    /**
     * The logger used to perform logging.
     * <p>
     * Initial value is null. Can be any value. If it is null then no logging will be performed. It has getter and
     * setter.
     */
    private Logger logger = LogUtil.getLogger(getClass().getSimpleName());

    /**
     * Default empty constructor.
     */
    public XREAggregateCommandHandler() {
        // empty
    }

    /**
     * Handle given command message.
     *
     * @param command the XRE command to be handled
     * @param receiver the receiver for the XRE application
     * @throws IllegalArgumentException if given command or receiver is null
     * @throws IllegalStateException if any of error handler, path evaluator or command handlers is not set
     */
    @SuppressWarnings("boxing")
    public void onCommandMessage(XRECommand command, AndroidReceiver receiver) {
        String methodName = "onCommandMessage";
        LogUtil.enter(logger, this, methodName, command, receiver);
        try {
            // check state
            CommandHandlersUtil.checkState(errorHandler, "errorHandler");
            CommandHandlersUtil.checkState(pathEvaluator, "pathEvaluator");
            CommandHandlersUtil.checkState(commandHandlers, "commandHandlers");

            // check arguments
            CommandHandlersUtil.checkNotNull(command, "command");
            CommandHandlersUtil.checkNotNull(receiver, "receiver");

            // AndroidAssembly2 changed to avoid double logging
            XRECommandHandler handler;
            try {
                // get application data
                ApplicationData applicationData = getApplicationData(command, receiver);

                // check command index
                int commandIndex = command.getCommandIndex();
                /*if (commandIndex != XRECommand.LOCAL_COMMAND_INDEX
                    && commandIndex != applicationData.getCurrentCommandIndex() + 1) {
                    // increase badCommandAcceptCount
                    applicationData.incrementBadCommandAcceptCount();

                    // handle error
                    errorHandler.processCommandError(new XREBadCommandIndexException(
                        "Bad command index received, expected " + (applicationData.getCurrentCommandIndex() + 1) + ".",
                        commandIndex), command, receiver);

                    // shut down the receiver if bad command count is reached
                    if (applicationData.getBadCommandAcceptCount() > receiver.getMaxBadCommandAcceptCount()) {
                        try {
                            LogUtil.warn(getLogger(), this, methodName,
                                "Shutting down receiver because of large number of bad commands.");
                            // shut down receiver
                            receiver.shutdown(false);

                            LogUtil.exit(logger, this, methodName);
                            return;
                        } catch (AndroidReceiverException e) {
                            throw new XRECommandHandlerException("Failed to shutdown receiver.", e);
                        }
                    }

                    // AndroidAssembly1 better to process command with incorrect index
                    // AndroidAssembly2 added logging of warning
                    LogUtil.warn(logger, this, methodName, "Processing command with incorrect index {0}", commandIndex);
                }*/

                // AndroidAssembly1 made command index start from 1
                if (commandIndex != XRECommand.LOCAL_COMMAND_INDEX || applicationData.getCurrentCommandIndex() == -1) {
                    // update current command index
                    applicationData.incrementCurrentCommandIndex();
                }

                // AndroidAssembly1 changed to use XRETargetCommand command
                if (command instanceof XRETargetCommand) {
                    processTargetCommand((XRETargetCommand) command, receiver);
                }

                if (command instanceof XRETargetCommand && ((XRETargetCommand) command).getTargetId() == 2105) {
                	processTargetCommand((XRETargetCommand) command, receiver);
            	}
                
                // get concrete handler
                String commandType = command.getCommand();
                handler = commandHandlers.get(commandType);
                if (handler == null) {
                    throw new XREUnrecognizedCommandException("Unrecognized command type '" + commandType + "'.",
                        commandType);
                }
            } catch (XRECommandHandlerException e) {
                LogUtil.error(logger, this, methodName, e);
                throw e;
            }

            // delegate to concrete handler
            handler.onCommandMessage(command, receiver);
            LogUtil.exit(logger, this, methodName);
        } catch (IllegalArgumentException e) {
            throw LogUtil.error(logger, this, methodName, e);
        } catch (IllegalStateException e) {
            throw LogUtil.error(logger, this, methodName, e);
        } catch (XRECommandHandlerException e) {
            // handle error
            errorHandler.processCommandError(e, command, receiver);
        }
    }

    /**
     * Gets the map from command type to command handlers.
     *
     * @return the map from command name to command handlers
     */
    public Map<String, XRECommandHandler> getCommandHandlers() {
        // return shallow copy
        return commandHandlers == null ? null : new HashMap<String, XRECommandHandler>(commandHandlers);
    }

    /**
     * Sets the map from command type to command handlers.
     *
     * @param commandHandlers the map from command type to command handlers
     * @throws IllegalArgumentException if given map is null or empty; or if it contain null/empty key; or if it
     * contains null value
     */
    public void setCommandHandlers(Map<String, XRECommandHandler> commandHandlers) {
        // check map
        CommandHandlersUtil.checkMap(commandHandlers, "commandHandlers");

        // set to shallow copy
        this.commandHandlers = new HashMap<String, XRECommandHandler>(commandHandlers);
    }

    /**
     * Gets the error handler.
     *
     * @return the error handler
     */
    public XRECommandErrorHandler getErrorHandler() {
        return errorHandler;
    }

    /**
     * Sets the error handler.
     *
     * @param errorHandler the error handler
     * @throws IllegalArgumentException if given handler is null
     */
    public void setErrorHandler(XRECommandErrorHandler errorHandler) {
        // check argument
        CommandHandlersUtil.checkNotNull(errorHandler, "errorHandler");

        this.errorHandler = errorHandler;
    }

    /**
     * Gets the evaluator used to evaluate the DOM path for XREObject.
     *
     * @return the evaluator used to evaluate the DOM path for XREObject
     */
    public PathEvaluator getPathEvaluator() {
        return pathEvaluator;
    }

    /**
     * Sets the evaluator used to evaluate the DOM path for XREObject.
     *
     * @param pathEvaluator the evaluator used to evaluate the DOM path for XREObject
     * @throws IllegalArgumentException if given evaluator is null
     */
    public void setPathEvaluator(PathEvaluator pathEvaluator) {
        // check argument
        CommandHandlersUtil.checkNotNull(pathEvaluator, "pathEvaluator");

        this.pathEvaluator = pathEvaluator;
    }

    /**
     * Gets the logger used to perform logging.
     *
     * @return the logger used to perform logging
     */
    public Logger getLogger() {
        return logger;
    }

    /**
     * Sets the logger used to perform logging.
     *
     * @param logger the logger used to perform logging
     */
    public void setLogger(Logger logger) {
        this.logger = logger;
    }

    /**
     * Process {@link XRECallCommand}, {@link XRESetCommand} and {@link XREDeleteCommand}.
     * <p>
     * AndroidAssembly1 changed to use target command
     *
     * @param command the command to process
     * @param receiver current receiver
     * @throws XRECommandHandlerException if path evaluator failed to evaluate target's path
     */
    private void processTargetCommand(XRETargetCommand command, AndroidReceiver receiver)
        throws XRECommandHandlerException {
        if (command.getTargetPath() != null && command.getTargetId() == XREObject.ID_NULL) {
            try {
                // evaluate the target path to id
                int targetId = pathEvaluator.evaluatePath(command.getTargetPath(), XREObject.ID_ROOT_VIEW, receiver);
                command.setTargetId(targetId);
            } catch (IllegalArgumentException e) {
                throw new XRECommandHandlerException("Illegal argument is passed to path evaluator.", e);
            } catch (InvalidAndroidReceiverStateException e) {
                throw new XRECommandHandlerException("Android receiver is not in correct state.", e);
            } catch (InvalidPathException e) {
                throw new XRECommandHandlerException("Target path '" + command.getTargetPath() + "' is invalid.", e);
            }
        }
    }

    /**
     * Gets application data.
     *
     * @param command the processed command
     * @param receiver current receiver
     * @return application data, maybe created on CONNECT command
     * @throws XREApplicationNotConnectedException if it is not CONNECT command and application data is not initialized
     */
    private static ApplicationData getApplicationData(XRECommand command, AndroidReceiver receiver)
        throws XREApplicationNotConnectedException {
        // get application data
        ApplicationData applicationData = receiver.getApplicationData();

        if (applicationData == null) {
            if (command instanceof XREConnectCommand) {
                // initialize the application data eagerly
                applicationData = new ApplicationData();
                receiver.setApplicationData(applicationData);

                // set command index to -1
                receiver.getApplicationData().setCurrentCommandIndex(
                    receiver.getApplicationConnectData().getCurrentCommandIndex());
            } else {
                throw new XREApplicationNotConnectedException(
                    "Application data is not initialized and not CONNECT command received.");
            }
        }
        return applicationData;
    }
}